InsertExecutor.java
package org.codefilarete.stalactite.engine.runtime;
import java.sql.Savepoint;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.codefilarete.stalactite.engine.VersioningStrategy;
import org.codefilarete.stalactite.mapping.EntityMapping;
import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
import org.codefilarete.stalactite.sql.ConnectionConfiguration;
import org.codefilarete.stalactite.sql.ConnectionProvider;
import org.codefilarete.stalactite.sql.RollbackListener;
import org.codefilarete.stalactite.sql.RollbackObserver;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.statement.ColumnParameterizedSQL;
import org.codefilarete.stalactite.sql.statement.DMLGenerator;
import org.codefilarete.stalactite.sql.statement.SQLOperation.SQLOperationListener;
import org.codefilarete.stalactite.sql.statement.SQLStatement.BindingException;
import org.codefilarete.stalactite.sql.statement.WriteOperation;
import org.codefilarete.stalactite.sql.statement.WriteOperationFactory;
import org.codefilarete.stalactite.sql.statement.WriteOperationFactory.ExpectedBatchedRowCountsSupplier;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.StringAppender;
import org.codefilarete.tool.collection.Arrays;
import org.codefilarete.tool.collection.Iterables;
/**
* Dedicated class to insert statement execution
*
* @param <C> entity type
* @param <I> identifier type
* @param <T> table type
* @author Guillaume Mary
*/
public class InsertExecutor<C, I, T extends Table<T>> extends WriteExecutor<C, I, T> implements org.codefilarete.stalactite.engine.InsertExecutor<C> {
/** Entity lock manager, default is no operation as soon as a {@link VersioningStrategy} is given */
private OptimisticLockManager<C, T> optimisticLockManager = (OptimisticLockManager<C, T>) OptimisticLockManager.NOOP_OPTIMISTIC_LOCK_MANAGER;
private final IdentifierInsertionManager<C, I> identifierInsertionManager;
private SQLOperationListener<Column<T, ?>> operationListener;
public InsertExecutor(EntityMapping<C, I, T> mappingStrategy, ConnectionConfiguration connectionConfiguration,
DMLGenerator dmlGenerator, WriteOperationFactory writeOperationFactory,
int inOperatorMaxSize) {
super(mappingStrategy, connectionConfiguration, dmlGenerator, writeOperationFactory, inOperatorMaxSize);
this.identifierInsertionManager = mappingStrategy.getIdMapping().getIdentifierInsertionManager();
}
public <V> void setVersioningStrategy(VersioningStrategy<C, V> versioningStrategy) {
if (!(getConnectionProvider() instanceof RollbackObserver)) {
throw new UnsupportedOperationException("Version control is only supported for " + Reflections.toString(ConnectionProvider.class)
+ " that implements " + Reflections.toString(RollbackObserver.class));
}
// we could have put the column as an attribute of the VersioningStrategy but, by making the column more dynamic, the strategy can be
// shared as long as PropertyAccessor is reusable over entities (wraps a common method)
Column<T, V> versionColumn = (Column<T, V>) getMapping().getVersioningMapping().getRight();
setOptimisticLockManager(new RevertOnRollbackMVCC<>(versioningStrategy, versionColumn, (RollbackObserver) getConnectionProvider()));
}
public void setOptimisticLockManager(OptimisticLockManager<C, T> optimisticLockManager) {
this.optimisticLockManager = optimisticLockManager;
}
public void setOperationListener(SQLOperationListener<Column<T, ?>> listener) {
this.operationListener = listener;
}
@Override
public void insert(Iterable<? extends C> entities) {
Set<Column<T, ?>> columns = getMapping().getInsertableColumns();
ColumnParameterizedSQL<T> insertStatement = getDmlGenerator().buildInsert(columns);
List<? extends C> entitiesCopy = Iterables.copy(entities);
ExpectedBatchedRowCountsSupplier expectedBatchedRowCountsSupplier = new ExpectedBatchedRowCountsSupplier(entitiesCopy.size(), getBatchSize());
WriteOperation<Column<T, ?>> writeOperation = getWriteOperationFactory()
.createInstanceForInsertion(insertStatement, getConnectionProvider(), expectedBatchedRowCountsSupplier);
writeOperation.setListener(this.operationListener);
JDBCBatchingIterator<C> jdbcBatchingIterator = identifierInsertionManager.buildJDBCBatchingIterator(entitiesCopy, writeOperation, getBatchSize());
jdbcBatchingIterator.forEachRemaining(entity -> {
try {
addToBatch(entity, writeOperation);
} catch (RuntimeException e) {
throw new RuntimeException("Error while inserting values for " + entity + " in statement \"" + writeOperation.getSqlStatement().getSQL() + "\"", e);
}
});
}
private void addToBatch(C entity, WriteOperation<Column<T, ?>> writeOperation) {
Map<Column<T, ?>, ?> insertValues = getMapping().getInsertValues(entity);
assertMandatoryColumnsHaveNonNullValues(insertValues);
optimisticLockManager.manageLock(entity, (Map<Column<T, ?>, Object>) insertValues);
writeOperation.addBatch(insertValues);
}
private void assertMandatoryColumnsHaveNonNullValues(Map<Column<T, ?>, ?> insertValues) {
Set<Column> nonNullColumnsWithNullValues = Iterables.collect(insertValues.entrySet(),
e -> Boolean.FALSE.equals(e.getKey().isNullable()) && e.getValue() == null, Entry::getKey, HashSet::new);
if (!nonNullColumnsWithNullValues.isEmpty()) {
throw new BindingException("Expected non null value for : "
// we sort result only to stabilize message for tests assertion, do not get it as a business rule
+ new StringAppender().ccat(Arrays.asTreeSet(Comparator.comparing(Column::getAbsoluteName), nonNullColumnsWithNullValues) , ", "));
}
}
/**
* The contract for managing Optimistic Lock on insert.
* @param <E> entity type
* @param <T> table type
*/
public interface OptimisticLockManager<E, T extends Table<T>> {
OptimisticLockManager<?, ?> NOOP_OPTIMISTIC_LOCK_MANAGER = (OptimisticLockManager) (o, m) -> {};
/**
* Expected to "manage" the optimistic lock:
* - can manage it thanks to a versioning column, then must upgrade the entity and takes connection rollback into account
* - can manage it by adding modified columns in the where clause
*
* @param instance
* @param updateValues
*/
// Note that generics syntax is made for write-only into the Map
void manageLock(E instance, Map<Column<T, ?>, Object> updateValues);
}
/**
* {@link OptimisticLockManager} that sets version value on entity and SQL order
* @param <V> version value type
* @author Guillaume Mary
*/
private class RevertOnRollbackMVCC<V> extends AbstractRevertOnRollbackMVCC<C, V, T> implements OptimisticLockManager<C, T> {
/**
* Main constructor.
*
* @param versioningStrategy the entities upgrader
* @param versionColumn the column that stores the version
* @param rollbackObserver the {@link RollbackObserver} to revert upgrade when rollback happens
* {@link ConnectionProvider#giveConnection()} is not used here, simple mark to help understanding
*/
private RevertOnRollbackMVCC(VersioningStrategy<C, V> versioningStrategy, Column<T, V> versionColumn, RollbackObserver rollbackObserver) {
super(versioningStrategy, versionColumn, rollbackObserver);
}
/**
* Upgrade inserted instance
*/
@Override
public void manageLock(C instance, Map<Column<T, ?>, Object> updateValues) {
V previousVersion = versioningStrategy.getVersion(instance);
this.versioningStrategy.upgrade(instance);
V newVersion = versioningStrategy.getVersion(instance);
updateValues.put(versionColumn, newVersion);
rollbackObserver.addRollbackListener(new VersioningStrategyRollbackListener<>(versioningStrategy, instance, previousVersion));
}
}
/**
* {@link RollbackListener} that reverts version upgrade on transaction rollback
* @param <C> entity type
* @param <V> version value type
*/
static class VersioningStrategyRollbackListener<C, V> implements RollbackListener {
private final VersioningStrategy<C, V> versioningStrategy;
private final C instance;
private final V previousVersion;
public VersioningStrategyRollbackListener(VersioningStrategy<C, V> versioningStrategy, C instance, V previousVersion) {
this.versioningStrategy = versioningStrategy;
this.instance = instance;
this.previousVersion = previousVersion;
}
@Override
public void beforeRollback() {
// no pre rollback treatment to do
}
@Override
public void afterRollback() {
// We revert the upgrade
versioningStrategy.revert(instance, previousVersion);
}
@Override
public void beforeRollback(Savepoint savepoint) {
// not implemented
}
@Override
public void afterRollback(Savepoint savepoint) {
// not implemented : should we do the same as default rollback ?
// it depends on if entity versioning was done during this savepoint ... how to know ?
}
@Override
public boolean isTemporary() {
// we don't need this on each rollback
return true;
}
}
}